﻿using MongoDB.Driver;
using Sustainalytics.ClientDW.Entities;
using Sustainalytics.DataLayer.CompanyHistory;
using Sustainalytics.DataLayer.Shared;
using Sustainalytics.Entities;
using Sustainalytics.Entities.ESGRatings;
using Sustainalytics.ESG.Entities;
using Sustainalytics.ESGRatings.DataLayer;
using Sustainalytics.MongoDB.Extensions;
using Sustainalytics.Synchronization.Interface;
using Sustainalytics.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace Sustainalytics.Synchronization.BusinessLogic
{
    public class HistoricalDataSyncStep : ISyncStep
    {
        private ICompanyHistoricalDataIndicatorsRepository _companyHistoricalDataIndicatorsRepository;
        private IRepositoryCollection<CompanyHistoricalPerformance> _companyHistoricalPerformanceRepository;
        private IMongoCollection<HistoricalData> _historicalDataCollection;
        private IRepositoryCollection<ESGProfile> _esgProfileRepository;
        private IRepositoryCollection<HistoricalDataSet> _historicalDataSetRepository;
        private IRepositoryCollection<Entities.ESGRatings.Historical.ESGProfile> _historicalEsgProfileRepository;
        private IRepositoryCollection<RPCompanyESGComputationResult> _rpCompanyEsgComputationResultRepository;

        public SyncStepOrder ExecutionOrder => SyncStepOrder.SyncHistoricalData;

        public static bool ShouldExecuteSync(
            bool executeAnyway,
            DateTime referenceDate,
            DateTime lastSyncDate,
            out DateTime syncDate,
            out DateTime storeDate)
        {
            var today = DateTime.Today.Date;
            if (GetFirstDayOfMonth(lastSyncDate.Date) < GetFirstDayOfMonth(today.Date) || lastSyncDate == default(DateTime))
            {
                syncDate = DateTime.Today.Date;
                storeDate = GetFirstDayOfMonth(DateTime.Today.Date);
                return true;
            }

            if (executeAnyway || referenceDate == default(DateTime))
            {
                syncDate = DateTime.Today;
                storeDate = GetFirstDayOfMonth(DateTime.Today);
                return true;
            }

            syncDate = referenceDate == default(DateTime) ? DateTime.Today : referenceDate;
            storeDate = referenceDate == default(DateTime)
                            ? GetFirstDayOfMonth(DateTime.Today.AddMonths(1))
                            : GetFirstDayOfMonth(referenceDate.AddMonths(1));

            if (referenceDate != default(DateTime) && IsLastDayOfMonth(referenceDate))
            {
                return true;
            }

            return false;
        }

        public Tuple<Guid, DateTime> GetLastAvailableEsgComputationInfo()
        {
            var esgComputationResult = this._rpCompanyEsgComputationResultRepository.ReadWhere(r => r.SetId != Guid.Empty, 0, 1);

            if (esgComputationResult != null)
            {
                var rpCompanyESGComputationResult = esgComputationResult.FirstOrDefault();
                if (rpCompanyESGComputationResult != null)
                {
                    var setId = rpCompanyESGComputationResult.SetId;
                    var dataSetInfo = this._historicalDataSetRepository.Read(setId);
                    if (dataSetInfo != null)
                    {
                        return new Tuple<Guid, DateTime>(dataSetInfo.Id, dataSetInfo.CreationDate);
                    }
                }
            }

            return new Tuple<Guid, DateTime>(Guid.Empty, default(DateTime));
        }

        public Tuple<Guid, DateTime> GetLastHistoricalSyncInfo()
        {
            var dataSetInfo = this._historicalDataSetRepository.ReadWhere(set => set.ToBePreserved, null, null, Ordering<HistoricalDataSet>.Descending(x => x.CreationDate));

            if (dataSetInfo != null)
            {
                var first = dataSetInfo.FirstOrDefault();

                if (first != null)
                {
                    return new Tuple<Guid, DateTime>(first.Id, first.CreationDate);
                }
            }

            return new Tuple<Guid, DateTime>(Guid.Empty, default(DateTime));
        }

        public SyncContext Start(SyncContext syncContext)
        {
            var sw = Stopwatch.StartNew();

            this.EnsureRepositories(syncContext);

            var executeAnyway = bool.Parse(ConfigurationManager.AppSettings["executehistorical"] ?? "false");

            if (executeAnyway)
            {
                syncContext.Log("Historycal data sync step executed on demand");
            }

            var dataInDb = this.GetLastAvailableEsgComputationInfo();

            var lastSyncInfo = this.GetLastHistoricalSyncInfo();

            DateTime syncDate, storeDate;

            var shouldSync = ShouldExecuteSync(executeAnyway, dataInDb.Item2, lastSyncInfo.Item2, out syncDate, out storeDate);

            syncContext.Log(string.Format("Historycal data is processed for date: {0}", syncDate));

            if (!shouldSync)
            {
                syncContext.Log("Historycal data sync step processing 0 entries");
                return syncContext;
            }

            var esgComputationResult = this._rpCompanyEsgComputationResultRepository.ReadAll().ToList();

            sw.Stop();
            syncContext.Log($"{esgComputationResult.Count} Company ESG computation results retrieved. Elapsed time: {sw.Elapsed}");

            var rpCompanyESGComputationResult = esgComputationResult.FirstOrDefault(x => x.SetId != Guid.Empty);

            if (rpCompanyESGComputationResult == null)
            {
                syncContext.Log("ATTENTION! No historical data set to copy from!");
                return syncContext;
            }

            var setId = rpCompanyESGComputationResult.SetId;

            syncContext.Log(string.Format("DataSetId: {0}", setId));

            var esgProfile = this._esgProfileRepository.ReadAll().FirstOrDefault(x => string.Equals(x.Name, "Default", StringComparison.InvariantCultureIgnoreCase));

            this.SyncESGProfile(syncContext, esgProfile, storeDate);
            this.SyncCompanyHistoricalDataIndicators(syncContext, esgComputationResult, storeDate);
            this.SyncCompanyHistoricalPerformance(syncContext, esgComputationResult, storeDate);

            syncContext.AddChange(new ChangedElement
            {
                AffectedObjectName = ChangedElement.ARCHIVE_DATA_SET_ID,
                AffectedDocumentId = setId,
            });

            return syncContext;
        }

        private static DateTime GetFirstDayOfMonth(DateTime date)
        {
            return new DateTime(date.Year, date.Month, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        }

        private static bool IsLastDayOfMonth(DateTime date)
        {
            if (date == default(DateTime))
            {
                return true;
            }

            var computedLastDay = GetFirstDayOfMonth(date).AddMonths(1).AddDays(-1);
            return date == computedLastDay;
        }

        private void EnsureRepositories(SyncContext syncContext)
        {
            if (syncContext.GAConnectionStrings.Any())
            {
                int commandTimeout = SyncHelper.ReadCommandTimeOutFromConfig();

                syncContext.Log(
                    string.Format("DataReader CommandTimeout: {0}", commandTimeout));

                string connectionString;

                if (!syncContext.GAConnectionStrings.TryGetValue("ClientDW", out connectionString))
                {
                    throw new ArgumentException("No connection string found for ClientDW!");
                }

                this._rpCompanyEsgComputationResultRepository = new RepositoryCollection<RPCompanyESGComputationResult>(connectionString);

                syncContext.Log("IRepositoryCollection<RPCompanyESGComputationResult> acquired based on connection string " + connectionString);

                if (!syncContext.GAConnectionStrings.TryGetValue("DWArchive", out connectionString))
                {
                    throw new ArgumentException("No connection string found for DWArchive!");
                }

                this._companyHistoricalDataIndicatorsRepository = new CompanyHistoricalDataIndicatorsRepository(connectionString);

                syncContext.Log("IRepositoryCollection<CompanyHistoricalDataIndicators> acquired based on connection string " + connectionString);

                this._companyHistoricalPerformanceRepository = new CompanyPerformanceHistoryRepository(connectionString);

                syncContext.Log("IRepositoryCollection<CompanyHistoricalPerformance> acquired based on connection string " + connectionString);

                this._historicalDataCollection = MongoFactory.GetCollectionFromDatabase<HistoricalData>(connectionString);

                syncContext.Log("IRepositoryCollection<ESGProfile> acquired based on connection string " + connectionString);

                this._historicalEsgProfileRepository = new RepositoryCollection<Entities.ESGRatings.Historical.ESGProfile>(connectionString);

                syncContext.Log("IRepositoryCollection<ESGProfile> acquired based on connection string " + connectionString);

                if (!syncContext.GAConnectionStrings.TryGetValue("ClientPlatform", out connectionString))
                {
                    throw new ArgumentException("No connection string found for ClientPlatform!");
                }

                this._esgProfileRepository = new RepositoryCollection<ESGProfile>(connectionString);
                syncContext.Log(
                    "IRepositoryCollection<ESGProfile> acquired based on connection string " + connectionString);

                if (!syncContext.GAConnectionStrings.TryGetValue("RPHistoricalDataSet", out connectionString))
                {
                    throw new ArgumentException("No connection string found for RPHistoricalDataSet");
                }

                this._historicalDataSetRepository = new RepositoryCollection<HistoricalDataSet>(connectionString);
            }
        }

        private void SyncCompanyHistoricalDataIndicators(SyncContext syncContext,
            IEnumerable<RPCompanyESGComputationResult> esgComputationResult, DateTime processedDate)
        {
            var sw = Stopwatch.StartNew();

            var dataIndicators = new ConcurrentBag<CompanyHistoricalDataIndicators>();

            var options = new ParallelOptions { CancellationToken = syncContext.Token };

            Parallel.ForEach(
                esgComputationResult,
                options,
                result =>
                //esgComputationResult.ForEach(result =>
                {
                    if (result.Indicators != null)
                    {
                        dataIndicators.Add(new CompanyHistoricalDataIndicators
                        {
                            Id = Guid.NewGuid(),
                            CompanyId = result.Id,
                            CapitalIQID = string.Empty,
                            Indent = result.Indent,
                            Indicators = result.Indicators.Select(i => new IndicatorData
                            {
                                IndicatorNumber = i.Code,
                                Weight = i.Weight,
                                RawScore = Convert.ToInt32(i.RawScore)
                            }).ToList(),
                            ProcessedDate = processedDate
                        });
                    }
                });

            if (dataIndicators != null && dataIndicators.Any())
                _companyHistoricalDataIndicatorsRepository.CreateBatch(dataIndicators);

            sw.Stop();
            syncContext.Log(string.Format("{0} CompanyHistoricalDataIndicators entities saved in DWArchive. Elapsed time: {1}",
                dataIndicators.Count, sw.Elapsed));
        }

        private void SyncCompanyHistoricalPerformance(SyncContext syncContext,
            IReadOnlyCollection<RPCompanyESGComputationResult> esgComputationResult, DateTime processedDate)
        {
            var sw = Stopwatch.StartNew();

            Parallel.ForEach(esgComputationResult, result =>
            {
                var chd = new CompanyHistoryData()
                {
                    Date = processedDate,
                    CompanyId = result.Id,
                };
                if (result.OverallPerformance != null)
                {
                    chd.OverallScore = result.OverallPerformance.Score;
                    chd.OverallAverage = result.OverallPerformance.Avg;
                    chd.OverallBestInClass = result.OverallPerformance.Max;
                }
                if (result.EnvironmentPerformance != null)
                {
                    chd.EnvironmentScore = result.EnvironmentPerformance.Score;
                    chd.EnvironmentAverage = result.EnvironmentPerformance.Avg;
                    chd.EnvironmentBestInClass = result.EnvironmentPerformance.Max;
                }
                if (result.SocialPerformance != null)
                {
                    chd.SocialScore = result.SocialPerformance.Score;
                    chd.SocialAverage = result.SocialPerformance.Avg;
                    chd.SocialBestInClass = result.SocialPerformance.Max;
                    chd.GovernanceScore = result.GovernancePerformance.Score;
                    chd.GovernanceAverage = result.GovernancePerformance.Avg;
                    chd.GovernanceBestInClass = result.GovernancePerformance.Max;
                }
                var chp = new CompanyHistoricalPerformance
                {
                    Id = result.Id,
                    Last36Months = new List<CompanyHistoryData>() { chd }
                };

                if (result.SetId != Guid.Empty)
                {
                    var historicalData = new HistoricalData(processedDate, result.Id)
                    {
                        OverallPercentile = result.OverallPerformance.Percentile,
                        OverallScore = result.OverallPerformance.Score,
                        EPercentile = result.EnvironmentPerformance.Percentile,
                        EScore = result.EnvironmentPerformance.Score,
                        SPercentile = result.SocialPerformance.Percentile,
                        SScore = result.SocialPerformance.Score,
                        GPercentile = result.GovernancePerformance.Percentile,
                        GScore = result.GovernancePerformance.Score,
                        Id = Guid.NewGuid(),
                        SetId = result.SetId
                    };
                    _historicalDataCollection.DeleteManyAsync(x => x.CompanyId == historicalData.CompanyId && x.Date == historicalData.Date).Wait();
                    _historicalDataCollection.InsertOneAsync(historicalData).Wait();
                }

                this._companyHistoricalPerformanceRepository.CreateOrUpdate(chp);
            });

            sw.Stop();
            syncContext.Log(string.Format("{0} CompanyHistoricalPerformance entities saved in DWArchive. Elapsed time: {1}",
                esgComputationResult.Count, sw.Elapsed));
        }

        private void SyncESGProfile(SyncContext syncContext, ESGProfile esgProfile, DateTime storeDate)
        {
            syncContext.Log(String.Format("Saving Esg profile with id {0}", esgProfile.Id));
            var historycalEsgProfile = new Entities.ESGRatings.Historical.ESGProfile(esgProfile, storeDate);
            this._historicalEsgProfileRepository.CreateOrUpdate(historycalEsgProfile);
            syncContext.Log(String.Format("EsgProfileSaved {0}", esgProfile.Id));
        }
    }
}
